<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VDO.Ninja OBS Control Dock</title>
    <style>
body { 
    font-family: sans-serif; 
    margin: 5px; 
    background-color: #13141A; 
    color: #e0e0e0;
    font-size: 13px; 
}
.container { 
    margin-bottom: 10px; 
    padding: 8px; 
    background-color: #272A33; 
    border-radius: 5px; 
    border: 1px solid #3a3a48;
}
.collapsible {
    cursor: pointer;
    user-select: none;
    padding: 8px 0;
    position: relative;
    font-weight: bold;
    background: rgba(255,255,255,0.03);
    margin: -8px -8px 8px -8px;
    padding-left: 12px;
    border-bottom: 1px solid #3a3a48;
}
.collapsible::after {
    content: '▼';
    position: absolute;
    right: 12px;
    font-size: 12px;
    color: #8a8a9a;
    transition: transform 0.2s ease;
}
.collapsible.collapsed::after {
    content: '►';
    transform: none;
}
.collapsible:hover {
    background: rgba(255,255,255,0.08);
}
.collapsible::before {
    content: 'Click to ' attr(data-state);
    position: absolute;
    right: 30px;
    font-size: 10px;
    color: #666;
    font-weight: normal;
}
.collapsible[data-state="expand"]::before {
    content: 'Click to expand';
}
.collapsible[data-state="collapse"]::before {
    content: 'Click to collapse';
}
.collapsible-content {
    max-height: 1000px;
    overflow: hidden;
    transition: max-height 0.2s ease-out;
    padding-top: 5px;
}
.collapsible-content.collapsed {
    max-height: 0;
    padding-top: 0;
}
label { 
    display: block; 
    margin-bottom: 5px; 
}
input[type="text"], input[type="password"], select {
    width: calc(100% - 16px); 
    padding: 5px; 
    margin-bottom: 8px;
    border: 1px solid #424254; 
    border-radius: 3px; 
    background-color: #3C404D; 
    color: #e0e0e0;
}
select {
    padding: 6px 5px;
    height: auto;
}
button {
    padding: 6px 10px; 
    background-color: #3C404D; 
    color: white;
    border: none; 
    border-radius: 3px; 
    cursor: pointer; 
    margin-right: 3px;
    margin-bottom: 3px;
    font-size: 12px;
}
button.connected { 
    background-color: #4C80AF; 
}
button.disconnected { 
    background-color: #484860;
}
button:hover { 
    background-color: #5a5a7a; 
}
.blur-field {
    filter: blur(5px);
    transition: filter 0.2s ease;
}
.blur-field:focus {
    filter: blur(0);
}
#vdoNinjaIframe { 
    width: 1px; 
    height: 1px; 
    position: absolute; 
    left: -1000px; 
    top: -1000px; 
    border: 0; 
}
.log-area {
    height: 100px; 
    background-color: #1a1a24; 
    color: #ccc; 
    border: 1px solid #424254;
    overflow-y: scroll; 
    padding: 5px; 
    font-family: monospace; 
    font-size: 0.9em;
    margin-top: 5px; 
    white-space: pre-wrap;
}
.status-indicator {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-left: 5px;
    background-color: #555;
}
.status-indicator.connected { 
    background-color: #4C80AF; 
}
.status-indicator.error { 
    background-color: #f44336; 
}
.stream-list { 
    max-height: 120px; 
    overflow-y: auto; 
    background-color: #1a1a24; 
    border: 1px solid #424254;
    border-radius: 3px;
    padding: 5px;
    margin-top: 3px;
}
.stream-item {
    padding: 4px;
    border-bottom: 1px solid #3c3c4a;
    font-size: 12px;
    word-break: break-word;
}
.stream-item:last-child {
    border-bottom: none;
}
h1, h2 {
    color: #c8c8c8;
    margin: 5px 0;
    font-size: 1.1em;
}
h1 {
    font-size: 1.2em;
}
small {
    color: #8a8a9a;
    font-size: 0.85em;
}
.add-stream-btn {
    background-color: #4C80AF;
    color: white;
    padding: 2px 4px;
    font-size: 11px;
}
.status-line {
    font-size: 12px;
    margin-top: 5px;
    display: flex;
    align-items: center;
}
input[type="checkbox"] {
    accent-color: #4C80AF;
    margin-right: 5px;
}
.checkbox-label {
    display: flex;
    align-items: center;
    margin-bottom: 3px;
}
.checkbox-label input {
    margin-right: 4px;
}
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}
::-webkit-scrollbar-track {
    background: #1a1a24;
    border-radius: 3px;
}
::-webkit-scrollbar-thumb {
    background: #3C404D;
    border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
    background: #4C80AF;
}
* {
    scrollbar-width: thin;
    scrollbar-color: #3C404D #1a1a24;
}
.flex-row {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}
#obsSceneNameInput {
    display: none !important;
}
#loadScenesBtn {
    display: inline-block;
    margin-left: 5px;
    vertical-align: top;
}
    </style>
</head>
<body>
    <h1>VDO.Ninja OBS Control</h1>

    <div class="container">
        <h2 class="collapsible" data-state="collapse">OBS WebSocket Connection</h2>
        <div class="collapsible-content">
            <label for="obsWsUrl">WebSocket URL:</label>
            <input type="text" id="obsWsUrl" value="ws://localhost:4455">

            <label for="obsWsPassword">Password:</label>
            <input type="password" id="obsWsPassword" value="">

            <div class="status-line">
				<button id="obsConnectBtn">Connect to OBS</button>
                <span id="obsConnectionStatus">Status: Disconnected</span>
                <span id="obsStatusIndicator" class="status-indicator"></span>
            </div>
        </div>
    </div>

    <div class="container">
        <h2 class="collapsible" data-state="collapse">VDO.Ninja Settings</h2>
        <div class="collapsible-content">
            <label for="vdoNinjaRoom">Room Name:</label>
            <input type="text" id="vdoNinjaRoom" placeholder="e.g., MyNinjaRoom" class="blur-field">

            <label for="vdoNinjaPassword">Password:</label>
            <input type="password" id="vdoNinjaPassword" placeholder="Room or &password">

            <label for="vdoNinjaStreamIds">Stream IDs:</label>
            <input type="text" id="vdoNinjaStreamIds" placeholder="streamId1,streamId2" class="blur-field">
            <small>Room Name or Stream ID(s) needed</small>
        </div>
    </div>

    <div class="container">
        <h2 class="collapsible" data-state="expand">OBS Target Settings</h2>
        <div class="collapsible-content collapsed">
            <label for="obsSceneSelect">Target Scene:</label>
            <div style="display: flex; align-items: center; gap: 5px;">
                <select id="obsSceneSelect" style="flex: 1;">
                    <option value="">Select a scene...</option>
                </select>
                <button id="loadScenesBtn">Re-Fetch Scenes</button>
            </div>
            <input type="text" id="obsSceneNameInput" style="display:none;">

            <label for="sourceSizing">New Source Sizing:</label>
			
			<select id="sourceSizing">
				<option value="autoGrid">Auto Grid Layout</option>
				<option value="bestFit">Best Fit (Preserve Aspect)</option>
				<option value="stretchToFill">Stretch to Fill Screen</option>
				<option value="defaultSize">Default (1920x1080 at 0,0)</option>
			</select>

            <div id="autoSourceOptions">
                <label class="checkbox-label">
                    <input type="checkbox" id="autoAddSources" checked>
                    Auto-add new streams as sources
                </label>
                <label class="checkbox-label">
                    <input type="checkbox" id="autoRemoveSources" checked>
                    Auto-remove sources on disconnect
                </label>
            </div>
        </div>
    </div>

    <div class="container">
        <h2 class="collapsible" data-state="expand">Stream ID Mappings</h2>
        <div class="collapsible-content collapsed">
            <div id="streamMappingContainer">
				<div id="streamMappings"></div>
				<button id="addStreamMappingBtn">Add New Mapping</button>
			</div>
            </div>
    </div>

    <div class="container">
        <h2 class="collapsible" data-state="collapse">Active Streams</h2>
        <div class="collapsible-content">
            <div id="streamList" class="stream-list">
                <div class="stream-item">No active streams</div>
            </div>
        </div>
    </div>

    <iframe id="vdoNinjaIframe" allow="encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;midi *;geolocation;camera *;microphone *;fullscreen;picture-in-picture;display-capture;accelerometer;autoplay;gyroscope;screen-wake-lock;"></iframe>

    <div class="container">
        <h2 class="collapsible" data-state="expand">Log</h2>
        <div class="collapsible-content collapsed">
            <div id="logArea" class="log-area"></div>
        </div>
    </div>

    <script>
// DOM elements
const obsWsUrlInput = document.getElementById('obsWsUrl');
const obsWsPasswordInput = document.getElementById('obsWsPassword');
const obsConnectBtn = document.getElementById('obsConnectBtn');
const obsConnectionStatus = document.getElementById('obsConnectionStatus');
const obsStatusIndicator = document.getElementById('obsStatusIndicator');

const vdoNinjaRoomInput = document.getElementById('vdoNinjaRoom');
const vdoNinjaPasswordInput = document.getElementById('vdoNinjaPassword');
const vdoNinjaStreamIdsInput = document.getElementById('vdoNinjaStreamIds');
const vdoNinjaIframe = document.getElementById('vdoNinjaIframe');

const obsSceneNameInput = document.getElementById('obsSceneNameInput');
const obsSceneSelect = document.getElementById('obsSceneSelect');
const sourceSizingSelect = document.getElementById('sourceSizing');
const autoAddSourcesCheckbox = document.getElementById('autoAddSources');
const autoRemoveSourcesCheckbox = document.getElementById('autoRemoveSources');
const streamListContainer = document.getElementById('streamList');
const logArea = document.getElementById('logArea');
const loadScenesBtn = document.getElementById('loadScenesBtn');
let vdoNinjaConnected = false;

// Set up collapsible sections
document.querySelectorAll('.collapsible').forEach(header => {
    header.addEventListener('click', function() {
        this.classList.toggle('collapsed');
        const content = this.nextElementSibling;
        if (content.classList.contains('collapsible-content')) {
            content.classList.toggle('collapsed');
            // Update the data-state attribute
            if (content.classList.contains('collapsed')) {
                this.setAttribute('data-state', 'expand');
            } else {
                this.setAttribute('data-state', 'collapse');
            }
        }
    });
});

const vdoNinjaConnectBtn = document.createElement('button');
vdoNinjaConnectBtn.id = 'vdoNinjaConnectBtn';
vdoNinjaConnectBtn.textContent = 'Connect to VDO.Ninja';
vdoNinjaConnectBtn.style.marginTop = '5px';

const vdoNinjaStatusIndicator = document.createElement('span');
vdoNinjaStatusIndicator.id = 'vdoNinjaStatusIndicator';
vdoNinjaStatusIndicator.className = 'status-indicator';

const vdoNinjaConnectionStatus = document.createElement('span');
vdoNinjaConnectionStatus.id = 'vdoNinjaConnectionStatus';
vdoNinjaConnectionStatus.textContent = 'Status: Disconnected';
vdoNinjaConnectionStatus.style.marginLeft = '5px';

// Add these elements after the vdoNinjaPassword input
const vdoNinjaSettingsContainer = document.querySelector('.container:nth-child(2)');
const buttonsDiv = document.createElement('div');
buttonsDiv.style.marginTop = '5px';
buttonsDiv.className = 'status-line';
buttonsDiv.appendChild(vdoNinjaConnectBtn);
buttonsDiv.appendChild(vdoNinjaConnectionStatus);
buttonsDiv.appendChild(vdoNinjaStatusIndicator);
vdoNinjaSettingsContainer.querySelector('.collapsible-content').appendChild(buttonsDiv);

vdoNinjaConnectBtn.addEventListener('click', () => {
    if (vdoNinjaConnected) {
        disconnectFromVdoNinja();
    } else {
        connectToVdoNinja();
    }
});

// State variables
let obs = null;
let obsConnected = false;
let activeStreams = {};
let obsScenes = [];
let requestCallbacks = {};

let vdoNinjaLastActivityTime = 0;
let vdoNinjaConnectionCheckTimer = null;

// Helper functions
function logMessage(message) {
	console.log(message);
	const timestamp = new Date().toLocaleTimeString();
	logArea.innerHTML += `[${timestamp}] ${message}\n`;
	logArea.scrollTop = logArea.scrollHeight;
}

function generateRequestId(type) {
	return `${type}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}

function toggleVdoNinjaInputs(disabled) {
    vdoNinjaRoomInput.disabled = disabled;
    vdoNinjaPasswordInput.disabled = disabled;
    vdoNinjaStreamIdsInput.disabled = disabled;
}
function updateVdoNinjaButtonState(connected) {
    vdoNinjaConnected = connected;
    
    if (connected) {
        vdoNinjaConnectBtn.textContent = 'Disconnect';
        vdoNinjaConnectBtn.classList.add('connected');
        vdoNinjaConnectBtn.classList.remove('disconnected');
        vdoNinjaConnectionStatus.textContent = 'Status: Connected';
        vdoNinjaStatusIndicator.classList.add('connected');
    } else {
        vdoNinjaConnectBtn.textContent = 'Connect to VDO.Ninja';
        vdoNinjaConnectBtn.classList.remove('connected');
        vdoNinjaConnectBtn.classList.add('disconnected');
        vdoNinjaConnectionStatus.textContent = 'Status: Disconnected';
        vdoNinjaStatusIndicator.classList.remove('connected');
    }
    
    // Toggle input fields based on connection state
    toggleVdoNinjaInputs(connected);
}

function saveSettings() {
    const settings = {
        obsWsUrl: obsWsUrlInput.value,
        obsWsPassword: obsWsPasswordInput.value,
        vdoNinjaRoom: vdoNinjaRoomInput.value,
        vdoNinjaPassword: vdoNinjaPasswordInput.value,
        vdoNinjaStreamIds: vdoNinjaStreamIdsInput.value,
        obsSceneName: obsSceneSelect.value,
        sourceSizing: sourceSizingSelect.value,
        autoAddSources: autoAddSourcesCheckbox.checked,
        autoRemoveSources: autoRemoveSourcesCheckbox.checked,
        vdoNinjaConnected: vdoNinjaConnected
        // Removed cloneToMainScene if you choose to remove the global checkbox
        // cloneToMainScene: document.getElementById('cloneToMainScene')?.checked || false 
    };
    localStorage.setItem('obsNinjaSettings', JSON.stringify(settings));
    
    const mappings = getStreamMappings();
    localStorage.setItem('obsNinjaStreamMappings', JSON.stringify(mappings));
}


function calculateGridPositions(totalSources, canvasWidth, canvasHeight) {
    const positions = [];
    
    let cols = Math.ceil(Math.sqrt(totalSources));
    let rows = Math.ceil(totalSources / cols);
    
    const cellWidth = canvasWidth / cols;
    const cellHeight = canvasHeight / rows;
    
    const lastRowItems = totalSources - ((rows - 1) * cols);
    const lastRowOffset = (cols - lastRowItems) * (cellWidth / 2);
    
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const index = row * cols + col;
            if (index < totalSources) {
                let xPos = col * cellWidth;
                if (row === rows - 1 && lastRowItems < cols) {
                    xPos += lastRowOffset;
                }
                positions.push({
                    x: xPos,
                    y: row * cellHeight,
                    width: cellWidth,
                    height: cellHeight
                });
            }
        }
    }
    return positions;
}
function disconnectFromVdoNinja() {
    vdoNinjaIframe.src = 'about:blank'; 
    if (vdoNinjaConnectionCheckTimer) {
        clearTimeout(vdoNinjaConnectionCheckTimer);
        vdoNinjaConnectionCheckTimer = null;
    }
    activeStreams = {};
    updateStreamList();
    updateVdoNinjaButtonState(false);
    saveSettings();
}

function connectToVdoNinja() {
    const room = vdoNinjaRoomInput.value.trim();
    const streamIds = vdoNinjaStreamIdsInput.value.trim();

    if (!room && !streamIds) {
        logMessage("VDO.Ninja Error: Room Name or Stream ID(s) must be provided.");
        return;
    }

    initializeVdoNinjaIframe();
    vdoNinjaConnectionStatus.textContent = 'Status: Connecting...';
    vdoNinjaConnectBtn.textContent = 'Cancel';
    
    if (vdoNinjaConnectionCheckTimer) {
        clearTimeout(vdoNinjaConnectionCheckTimer);
    }
    vdoNinjaConnectionCheckTimer = setTimeout(() => {
        if (Date.now() - vdoNinjaLastActivityTime > 10000) {
            logMessage("VDO.Ninja connection timed out. No activity received.");
            vdoNinjaConnectionStatus.textContent = 'Status: Connection Failed';
            vdoNinjaConnectBtn.textContent = 'Connect to VDO.Ninja';
        }
    }, 10000);
    toggleVdoNinjaInputs(true);
    saveSettings();
}

function addNewStreamMapping(streamId = '', label = '', sceneName = '', matchType = 'streamId', shouldClone = true, shouldSwitch = false) {
    const streamMappings = document.getElementById('streamMappings');
    const mappingDiv = document.createElement('div');
    mappingDiv.className = 'stream-mapping';
    mappingDiv.style.margin = '5px 0';
    
    mappingDiv.innerHTML = `
        <div style="margin-bottom: 5px;">
            <label style="font-size: 11px; margin-bottom: 2px; display: block;">Stream Mapping</label>
            <div class="flex-row" style="align-items: center; flex-wrap: nowrap; margin-bottom: 2px;">
                <input type="text" placeholder="Stream ID" value="${streamId instanceof PointerEvent ? '' : streamId}" class="mapping-stream-id" style="width:80px; margin-right: 4px;">
                <input type="text" placeholder="Label (optional)" value="${label}" class="mapping-label" style="width:120px; margin-right: 4px;">
                <select class="mapping-match-type" style="width:90px; margin-right: 4px;">
                    <option value="streamId" ${matchType === 'streamId' ? 'selected' : ''}>ID Only</option>
                    <option value="label" ${matchType === 'label' ? 'selected' : ''}>Label Only</option>
                    <option value="both" ${matchType === 'both' ? 'selected' : ''}>Both Required</option>
                    <option value="either" ${matchType === 'either' ? 'selected' : ''}>Either Match</option>
                </select>
                <select class="mapping-scene-name" style="width:130px;">
                    <option value="">Select a scene...</option>
                </select>
                <button class="remove-mapping-btn" style="width:auto; padding:3px 5px; margin-left: 4px;">×</button>
            </div>
            <div class="flex-row" style="margin-top: 3px; gap: 8px;">
                <label class="checkbox-label" style="margin-bottom: 0;">
                    <input type="checkbox" class="mapping-clone-to-main" ${shouldClone ? 'checked' : ''}>
                    Clone to main scene
                </label>
                <label class="checkbox-label" style="margin-bottom: 0;">
                    <input type="checkbox" class="mapping-switch-to-scene" ${shouldSwitch ? 'checked' : ''}>
                    Switch to scene on add
                </label>
            </div>
            <small style="color: #8a8a9a; font-size: 10px; display: block; margin-top: 2px;">
                ID Only: Match by Stream ID only | Label Only: Match by label only | 
                Both Required: Must match both | Either Match: Match if either matches
            </small>
        </div>
    `;
    
    streamMappings.appendChild(mappingDiv);
    const sceneDropdown = mappingDiv.querySelector('.mapping-scene-name');
    
    // Populate dropdown with existing scenes
    populateSceneDropdown(obsScenes, sceneDropdown);
    
    // Set the scene name if provided and valid
    if (sceneName && obsScenes.some(scene => scene.sceneName === sceneName)) {
        sceneDropdown.value = sceneName;
    }
    
    const removeBtn = mappingDiv.querySelector('.remove-mapping-btn');
    removeBtn.addEventListener('click', () => {
        mappingDiv.remove();
        saveSettings();
    });
    
    const inputs = mappingDiv.querySelectorAll('input, select');
    inputs.forEach(input => {
        input.addEventListener('change', saveSettings);
    });
}


function setupStreamMappingUI() {
    const streamMappingContainer = document.getElementById('streamMappingContainer');
    const addStreamMappingBtn = document.getElementById('addStreamMappingBtn');
    
    // If you decide to remove the global "cloneToMainScene" checkbox, remove the following block
    /*
    const cloneCheckboxDiv = document.createElement('div');
    cloneCheckboxDiv.innerHTML = `
        <label class="checkbox-label">
            <input type="checkbox" id="cloneToMainScene">
            Also clone mapped streams to main scene
        </label>
    `;
    // Ensure streamMappingContainer.parentNode exists if you keep this
    if (streamMappingContainer.parentNode) {
        streamMappingContainer.parentNode.insertAdjacentElement('afterend', cloneCheckboxDiv);
        const cloneCheckbox = document.getElementById('cloneToMainScene');
        if (cloneCheckbox) {
            cloneCheckbox.addEventListener('change', saveSettings);
        }
    }
    */
    
    // Corrected event listener for adding new stream mapping
    addStreamMappingBtn.addEventListener('click', () => {
        addNewStreamMapping(); // Call without arguments
    });
    
    loadStreamMappings();
}

function loadSettings() {
    const settingsJson = localStorage.getItem('obsNinjaSettings');
    if (settingsJson) {
        try {
            const settings = JSON.parse(settingsJson);
            obsWsUrlInput.value = settings.obsWsUrl || '';
            obsWsPasswordInput.value = settings.obsWsPassword || '';
            vdoNinjaRoomInput.value = settings.vdoNinjaRoom || '';
            vdoNinjaPasswordInput.value = settings.vdoNinjaPassword || '';
            vdoNinjaStreamIdsInput.value = settings.vdoNinjaStreamIds || '';
            obsSceneSelect.value = settings.obsSceneName || '';
            sourceSizingSelect.value = settings.sourceSizing || 'autoGrid';
            autoAddSourcesCheckbox.checked = settings.autoAddSources !== false;
            autoRemoveSourcesCheckbox.checked = settings.autoRemoveSources !== false;
            
            // Removed loading for 'cloneToMainScene' if you remove the global checkbox
            /*
            if (document.getElementById('cloneToMainScene') && typeof settings.cloneToMainScene !== 'undefined') {
                document.getElementById('cloneToMainScene').checked = settings.cloneToMainScene;
            }
            */
            
            if (settings.vdoNinjaConnected) {
                // connectToVdoNinja(); // User can reconnect manually
            }
        } catch (e) {
            logMessage(`Error loading settings: ${e.message}`);
            localStorage.removeItem('obsNinjaSettings');
        }
    } else {
        sourceSizingSelect.value = 'autoGrid';
    }
    setupStreamMappingUI(); // Call this after other settings are potentially loaded
}


function updateStreamList() {
    console.log("Updating stream list with:", activeStreams);
    if (Object.keys(activeStreams).length === 0) {
        streamListContainer.innerHTML = '<div class="stream-item">No active streams</div>';
        return;
    }

    streamListContainer.innerHTML = '';
    for (const streamId in activeStreams) {
        const stream = activeStreams[streamId];
        const streamDiv = document.createElement('div');
        streamDiv.className = 'stream-item';
        
        const targetInfo = getTargetSceneForStream(streamId, stream.label);
        const targetSceneName = targetInfo.scene; // This is the scene name string
        const isDefaultScene = targetSceneName === getTargetScene();
        
        streamDiv.innerHTML = `
            <div style="font-weight: bold;">${stream.label || streamId}</div>
            <small>ID: ${streamId}${stream.label ? ` | Label: ${stream.label}` : ''}</small>
            <small style="display: block; color: #8a8a9a;">
                → Target Scene: ${targetSceneName} ${isDefaultScene ? '(default)' : '(mapped)'}
            </small>
            ${stream.sourceCreated ? '<span style="color:#4CAF50"> ✓ Added to OBS</span>' : ''}
            <button class="add-stream-btn" data-stream-id="${streamId}" style="margin-top: 3px;">Add to OBS</button>
        `;
        streamListContainer.appendChild(streamDiv);

        const btn = streamDiv.querySelector('.add-stream-btn');
        btn.addEventListener('click', () => {
            // Pass the targetInfo object which contains { scene: sceneName, mapping: mappingObject }
            addStreamToObs(streamId, stream.label, targetInfo);
        });
    }
    console.log("Stream list updated:", streamListContainer.innerHTML);
}

obsConnectBtn.addEventListener('click', () => {
    if (obsConnected && obs) {
        logMessage("Disconnecting from OBS WebSocket...");
        if (obs) {
            obs.close();
            obs = null;
        }
    } else {
        connectToOBS();
    }
});

function getTargetSceneForStream(streamId, streamLabel = '') {
    const mappings = getStreamMappings();
    const defaultTargetScene = getTargetScene(); // The scene selected in the main "Target Scene" dropdown

    for (const mapping of mappings) {
        let isMatch = false;
        switch (mapping.matchType) {
            case 'streamId': isMatch = mapping.streamId && streamId === mapping.streamId; break;
            case 'label':    isMatch = mapping.label && streamLabel && streamLabel === mapping.label; break;
            case 'both':     isMatch = mapping.streamId && mapping.label && streamId === mapping.streamId && streamLabel === mapping.label; break;
            case 'either':   isMatch = (mapping.streamId && streamId === mapping.streamId) || (mapping.label && streamLabel && streamLabel === mapping.label); break;
        }
        if (isMatch && mapping.sceneName) { // Ensure mapping has a scene
            return { scene: mapping.sceneName, mapping: mapping }; // Return object with scene name and full mapping
        }
    }
    return { scene: defaultTargetScene, mapping: null }; // Default to main target scene if no match or mapping has no scene
}

function updateSceneDropdowns() {
    // Store current values before updating dropdowns
    const currentMainValue = obsSceneSelect.value;
    const mappingSelects = document.querySelectorAll('.mapping-scene-name');
    const currentMappingValues = Array.from(mappingSelects).map(select => select.value);
    
    // Update main scene dropdown
    populateSceneDropdown(obsScenes, obsSceneSelect);
    
    // Restore main scene selection if still valid
    if (currentMainValue && obsScenes.some(scene => scene.sceneName === currentMainValue)) {
        obsSceneSelect.value = currentMainValue;
    }
    
    // Update and restore mapping scene dropdowns
    mappingSelects.forEach((select, index) => {
        populateSceneDropdown(obsScenes, select);
        if (currentMappingValues[index] && obsScenes.some(scene => scene.sceneName === currentMappingValues[index])) {
            select.value = currentMappingValues[index];
        }
    });
}
function loadStreamMappings() {
    const settingsJson = localStorage.getItem('obsNinjaStreamMappings');
    if (settingsJson) {
        try {
            const mappings = JSON.parse(settingsJson);
            for (const mapping of mappings) {
                addNewStreamMapping(
                    mapping.streamId, 
                    mapping.label, 
                    mapping.sceneName, 
                    mapping.matchType,
                    mapping.cloneToMain !== undefined ? mapping.cloneToMain : true,
                    mapping.switchToScene !== undefined ? mapping.switchToScene : false
                );
            }
            
            // If scenes are already loaded, update the dropdowns
            if (obsScenes && obsScenes.length > 0) {
                updateSceneDropdowns();
            }
        } catch (e) {
            logMessage(`Error loading stream mappings: ${e.message}`);
        }
    }
}
function getStreamMappings() {
    const mappings = [];
    const mappingDivs = document.querySelectorAll('.stream-mapping');
    mappingDivs.forEach(div => {
        const streamId = div.querySelector('.mapping-stream-id').value.trim();
        const label = div.querySelector('.mapping-label').value.trim();
        const matchType = div.querySelector('.mapping-match-type').value;
        const sceneName = div.querySelector('.mapping-scene-name').value.trim();
        const cloneToMain = div.querySelector('.mapping-clone-to-main').checked;
        const switchToScene = div.querySelector('.mapping-switch-to-scene').checked;
        if (sceneName && (streamId || label)) { // Ensure a scene and some identifier exists
            mappings.push({ streamId, label, matchType, sceneName, cloneToMain, switchToScene });
        }
    });
    return mappings;
}

async function connectToOBS() {
    let url = obsWsUrlInput.value.trim();
    const password = obsWsPasswordInput.value;
    if (!url) {
        logMessage("Error: OBS WebSocket URL is required.");
        return;
    }
    if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
        url = 'ws://' + url;
        obsWsUrlInput.value = url; 
    }
    obsConnectionStatus.textContent = 'Status: Connecting...';
    obsStatusIndicator.classList.remove('connected', 'error');
    logMessage(`Attempting to connect to OBS at ${url}...`);
    
    const connectionTimeoutId = setTimeout(() => {
        if (obs && obs.readyState !== WebSocket.OPEN) {
            logMessage("Connection attempt timed out");
            if (obs) { try { obs.close(); } catch (e) {} obs = null; }
            obsConnectionStatus.textContent = 'Status: Error - Connection timed out';
            obsStatusIndicator.classList.add('error');
        }
    }, 10000); 
    
    try {
        obs = new WebSocket(url);
        obs.onopen = () => logMessage("WebSocket connection opened. Waiting for Hello message...");
        obs.onmessage = async (event) => {
            try {
                const message = JSON.parse(event.data);
                if (message.op === 0) { // Hello
                    logMessage("Received Hello from OBS WebSocket server");
                    // logMessage(JSON.stringify(message)); // Optional: raw hello message
                    try {
                        const identifyPayload = { op: 1, d: { rpcVersion: 1, eventSubscriptions: (1 << 0) | (1 << 1) } };
                        if (message.d && message.d.authentication) {
                            const { challenge, salt } = message.d.authentication;
                            if (password) {
                                identifyPayload.d.authentication = await generateAuthResponse(password, salt, challenge);
                                logMessage("Authentication data prepared");
                            } else { logMessage("Warning: Server requires authentication but no password provided"); }
                        }
                        // logMessage("Sending Identify message: " + JSON.stringify(identifyPayload)); // Optional: raw identify
                        obs.send(JSON.stringify(identifyPayload));
                    } catch (error) {
                        logMessage(`Error during authentication setup: ${error.message}`);
                        if (obs) obs.close();
                    }
                } else if (message.op === 2) { // Identified (auth success)
                    clearTimeout(connectionTimeoutId);
                    logMessage("OBS Authentication successful!");
                    obsConnected = true;
                    obsConnectBtn.textContent = 'Disconnect';
                    obsConnectBtn.classList.add('connected');
                    obsConnectBtn.classList.remove('disconnected');
                    obsConnectionStatus.textContent = 'Status: Connected';
                    obsStatusIndicator.classList.add('connected');
                    onObsConnected();
                } else if (message.op === 7) { // RequestResponse
                    if (message.d && message.d.requestId && requestCallbacks[message.d.requestId]) {
                        requestCallbacks[message.d.requestId](message.d);
                        delete requestCallbacks[message.d.requestId];
                    }
                    if (message.d && message.d.requestStatus && message.d.requestStatus.code !== 100) {
                        logMessage(`OBS Request Error (${message.d.requestType || 'Unknown'}): ${message.d.requestStatus.comment || 'Unknown error'}`);
                    }
                } else if (message.op === 5) { // Event
                    logMessage(`Received event: ${message.d ? message.d.eventType : 'Unknown'}`);
                } else {
                    // logMessage(`Received message op ${message.op}: ${JSON.stringify(message)}`); // Optional: other messages
                }
            } catch (error) { logMessage(`Error processing WebSocket message: ${error.message}`); }
        };
        obs.onerror = (error) => {
            clearTimeout(connectionTimeoutId);
            logMessage(`OBS WebSocket Error: ${error.message || 'Unknown WebSocket error'}`);
            obsStatusIndicator.classList.add('error');
            obsConnectionStatus.textContent = 'Status: Error';
        };
        obs.onclose = (event) => {
            clearTimeout(connectionTimeoutId);
            let closeReason = event.code ? `Code: ${event.code}` : 'Unknown reason';
            if (event.code === 4009) closeReason = 'Authentication Failed - incorrect password';
            logMessage(`OBS WebSocket Connection Closed. ${closeReason}`);
            obsConnected = false;
            obsConnectBtn.textContent = 'Connect to OBS';
            obsConnectBtn.classList.remove('connected');
            obsConnectBtn.classList.add('disconnected');
            obsConnectionStatus.textContent = 'Status: Disconnected';
            obsStatusIndicator.classList.remove('connected', 'error');
            onObsDisconnected();
        };
    } catch (error) {
        clearTimeout(connectionTimeoutId);
        logMessage(`Error creating WebSocket connection: ${error.message}`);
        obsConnectionStatus.textContent = 'Status: Error';
        obsStatusIndicator.classList.add('error');
    }
}

async function generateAuthResponse(password, salt, challenge) {
    try {
        const encoder = new TextEncoder();
        const secretString = password + salt;
        const secretData = encoder.encode(secretString);
        let secretHash;
        if (window.crypto && window.crypto.subtle) {
            const hashBuffer = await window.crypto.subtle.digest('SHA-256', secretData);
            secretHash = new Uint8Array(hashBuffer);
        } else {
            await loadJsShaLibrary(); // Ensure jsSHA is loaded
            const shaObj = new jsSHA("SHA-256", "TEXT", { encoding: "UTF8" });
            shaObj.update(secretString);
            const hashHex = shaObj.getHash("HEX");
            secretHash = new Uint8Array(hashHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
        }
        const secretBase64 = btoa(String.fromCharCode.apply(null, secretHash));
        const authString = secretBase64 + challenge;
        const authData = encoder.encode(authString);
        let authHash;
        if (window.crypto && window.crypto.subtle) {
            const hashBuffer = await window.crypto.subtle.digest('SHA-256', authData);
            authHash = new Uint8Array(hashBuffer);
        } else {
            // jsSHA already loaded
            const shaObj = new jsSHA("SHA-256", "TEXT", { encoding: "UTF8" });
            shaObj.update(authString);
            const hashHex = shaObj.getHash("HEX");
            authHash = new Uint8Array(hashHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
        }
        return btoa(String.fromCharCode.apply(null, authHash));
    } catch (error) {
        logMessage(`Auth generation error: ${error.message}`);
        throw error;
    }
}

function sendRequest(requestType, requestData = {}) {
	return new Promise((resolve, reject) => {
		if (!obsConnected || !obs) { reject(new Error("Not connected to OBS")); return; }
		const requestId = generateRequestId(requestType);
		requestCallbacks[requestId] = (response) => {
			if (response.requestStatus.code === 100) resolve(response.responseData || {});
			else reject(new Error(`Request ${requestType} failed: ${response.requestStatus.comment}`));
		};
		const request = { op: 6, d: { requestType, requestId, requestData } };
		try { obs.send(JSON.stringify(request)); } 
        catch (error) { delete requestCallbacks[requestId]; reject(error); }
		setTimeout(() => {
			if (requestCallbacks[requestId]) {
				delete requestCallbacks[requestId];
				reject(new Error(`Request timeout for ${requestType}`));
			}
		}, 5000);
	});
}

function onObsConnected() {
    logMessage("OBS Connected. Fetching scenes...");
    
    // Fetch scenes first, then handle VDO.Ninja connection
    fetchObsScenes().then(() => {
        // Now that scenes are loaded, check if VDO.Ninja should be reconnected
        const settingsJson = localStorage.getItem('obsNinjaSettings');
        if (settingsJson) {
            try {
                const settings = JSON.parse(settingsJson);
                if (settings.vdoNinjaConnected && (settings.vdoNinjaRoom || settings.vdoNinjaStreamIds)) {
                    connectToVdoNinja();
                }
            } catch (e) { 
                logMessage(`Error re-connecting VDO.Ninja: ${e.message}`); 
            }
        }
        
        // Apply auto-grid after everything is loaded
        if (sourceSizingSelect.value === 'autoGrid') {
            setTimeout(rearrangeAllStreamsInScene, 1000, getTargetScene()); 
        }
    });
}
function onObsDisconnected() {
    logMessage("OBS Disconnected.");
    for (const streamId in activeStreams) {
        activeStreams[streamId].sourceCreated = false;
    }
    updateStreamList();
}

async function fetchObsScenes() {
    if (!obsConnected || !obs) return;
    logMessage("Fetching OBS scenes...");
    try {
        const response = await sendRequest('GetSceneList');
        if (response && response.scenes) {
            obsScenes = response.scenes;
            updateSceneDropdowns();
            
            // Properly restore main scene selection after scenes are loaded
            const savedSettings = localStorage.getItem('obsNinjaSettings');
            if (savedSettings) {
                try {
                    const settings = JSON.parse(savedSettings);
                    if (settings.obsSceneName && obsScenes.some(s => s.sceneName === settings.obsSceneName)) {
                        obsSceneSelect.value = settings.obsSceneName;
                    }
                } catch (e) {
                    logMessage(`Error restoring scene selection: ${e.message}`);
                }
            }
            
            // Restore mapping scene selections after scenes are loaded
            const savedMappings = localStorage.getItem('obsNinjaStreamMappings');
            if (savedMappings) {
                try {
                    const mappings = JSON.parse(savedMappings);
                    const mappingSceneSelects = document.querySelectorAll('.mapping-scene-name');
                    mappingSceneSelects.forEach((select, index) => {
                        if (mappings[index] && mappings[index].sceneName) {
                            if (obsScenes.some(scene => scene.sceneName === mappings[index].sceneName)) {
                                select.value = mappings[index].sceneName;
                            }
                        }
                    });
                } catch (e) {
                    logMessage(`Error restoring mapping scene selections: ${e.message}`);
                }
            }
        }
    } catch (error) { 
        logMessage(`Error fetching OBS scenes: ${error.message}`); 
    }
}


function populateSceneDropdown(scenesData, selectElement = obsSceneSelect) {
    const currentValue = selectElement.value;
    selectElement.innerHTML = '<option value="">Select a scene...</option>';
    
    if (!scenesData || scenesData.length === 0) {
        return;
    }
    
    scenesData.forEach(scene => {
        const option = document.createElement('option');
        option.value = scene.sceneName;
        option.textContent = scene.sceneName;
        selectElement.appendChild(option);
    });
    
    // Restore the value if it still exists in the list
    if (currentValue && scenesData.some(scene => scene.sceneName === currentValue)) {
        selectElement.value = currentValue;
    }
}

function getTargetScene() { return obsSceneSelect.value || ''; }

function getVdoNinjaViewUrl(streamId, includeCommonParams = true) {
    const room = vdoNinjaRoomInput.value.trim();
    const ninjaPassword = vdoNinjaPasswordInput.value;
    let url = "https://vdo.ninja/?";
    if (room) url += `view=${encodeURIComponent(streamId)}&solo&room=${encodeURIComponent(room)}`;
    else url += `view=${encodeURIComponent(streamId)}`;
    if (ninjaPassword) url += `&password=${encodeURIComponent(ninjaPassword)}`;
    if (includeCommonParams) url += "&cleanoutput&proaudio&ab=160&transparent&autoplay&codec=h264";
    return url;
}

function initializeVdoNinjaIframe() {
    const room = vdoNinjaRoomInput.value.trim();
    const streamIds = vdoNinjaStreamIdsInput.value.trim();
    if (!room && !streamIds) { logMessage("VDO.Ninja: Room or Stream ID(s) needed."); return; }
    let vdoNinjaUrl = "https://vdo.ninja/?";
    if (room) {
        vdoNinjaUrl += `room=${encodeURIComponent(room)}`;
        if (streamIds) vdoNinjaUrl += `&view=${encodeURIComponent(streamIds.split(',').map(s => s.trim()).join(','))}&solo`;
    } else if (streamIds) {
        vdoNinjaUrl += `view=${encodeURIComponent(streamIds.split(',').map(s => s.trim()).join(','))}`;
    }
    if (vdoNinjaPasswordInput.value) vdoNinjaUrl += `&password=${encodeURIComponent(vdoNinjaPasswordInput.value)}`;
    vdoNinjaUrl += "&cleanoutput&dataonly&nocursor&nopush&noaudio&novideo&cors="+encodeURIComponent(window.location.origin);
    logMessage(`Loading VDO.Ninja iframe: ${vdoNinjaUrl}`);
    vdoNinjaIframe.src = 'about:blank'; 
    vdoNinjaIframe.setAttribute('crossorigin', 'anonymous');
    vdoNinjaIframe.setAttribute('allow', 'autoplay; camera; microphone; fullscreen; display-capture; clipboard-write');
    // Sandbox might be too restrictive for some VDO.Ninja features, but good for security
    // vdoNinjaIframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups'); 
    setTimeout(() => { vdoNinjaIframe.src = vdoNinjaUrl; }, 10);
    saveSettings();
}

window.addEventListener("message", (event) => {
    if (!event.origin.startsWith("https://vdo.ninja") || event.source !== vdoNinjaIframe.contentWindow) return;
    const data = event.data;
    vdoNinjaLastActivityTime = Date.now();
    if (!vdoNinjaConnected) {
        updateVdoNinjaButtonState(true);
        if (vdoNinjaConnectionCheckTimer) { clearTimeout(vdoNinjaConnectionCheckTimer); vdoNinjaConnectionCheckTimer = null; }
        logMessage("VDO.Ninja connection established.");
    }
    // if (data.action !== 'view-stats-updated') console.log("VDO.Ninja message:", data);

    if (data && data.streamID) {
        const streamId = data.streamID;
        const label = data.label || `Stream ${streamId}`;
        
        if ((data.action === "view-connection" && data.value === true) || 
            (data.action === "guest-connected") || 
            (data.action === "push-connection" && data.value === true)) {
            logMessage(`VDO.Ninja stream connected: ${label} (ID: ${streamId})`);
            activeStreams[streamId] = { label, sourceCreated: false, streamId, uuid: data.UUID || null, connected: true };
            updateStreamList();
            if (autoAddSourcesCheckbox.checked) {
                const targetInfo = getTargetSceneForStream(streamId, label);
                addStreamToObs(streamId, label, targetInfo);
            }
        } else if ((data.action === "view-connection" && data.value === false) || 
                   (data.action === "push-connection" && data.value === false)) {
            if (activeStreams[streamId]) {
                logMessage(`VDO.Ninja stream disconnected: ${label} (ID: ${streamId})`);
                if (autoRemoveSourcesCheckbox && autoRemoveSourcesCheckbox.checked) {
                    removeStreamFromObs(streamId); // Pass targetInfo if available or let removeStreamFromObs determine it
                }
                delete activeStreams[streamId];
                updateStreamList();
            }
        } else if (data.action === "view-connection-info" && data.value && data.value.label) {
            if (activeStreams[streamId]) {
                activeStreams[streamId].label = data.value.label;
                updateStreamList();
            }
        }
    }
}, false);

function startVdoNinjaConnectionMonitor() {
    setInterval(() => {
        if (vdoNinjaConnected && Date.now() - vdoNinjaLastActivityTime > 45000) {
            logMessage("VDO.Ninja connection lost (no activity).");
            vdoNinjaConnectionStatus.textContent = 'Status: Connection Lost';
            vdoNinjaStatusIndicator.classList.remove('connected');
            vdoNinjaConnectBtn.textContent = 'Reconnect';
            // Consider calling disconnectFromVdoNinja() or parts of it if full reset is desired
        }
    }, 30000);
}

async function addStreamToObs(streamId, streamLabel, targetInfo = null) {
    if (!obsConnected || !obs) { logMessage("Cannot add stream: Not connected to OBS."); return; }

    const resolvedTargetInfo = targetInfo || getTargetSceneForStream(streamId, streamLabel);
    const targetSceneName = resolvedTargetInfo.scene;
    const mappingRule = resolvedTargetInfo.mapping;

    if (!targetSceneName) { logMessage("Cannot add stream: Target OBS scene name is required."); return; }

    const sourceName = `VDO.Ninja_${streamId}`;
    const mainDefaultScene = getTargetScene();
    
    let shouldCloneThisStreamToMain = false;
    if (mappingRule && mappingRule.sceneName !== mainDefaultScene && mappingRule.cloneToMain) {
        shouldCloneThisStreamToMain = true;
    }

    let shouldSwitchToThisScene = false;
    if (mappingRule && mappingRule.switchToScene) {
        shouldSwitchToThisScene = true;
    }

    const vdoNinjaStreamUrl = getVdoNinjaViewUrl(streamId);
    let canvasWidth = 1920, canvasHeight = 1080;
    try {
        const videoSettings = await sendRequest('GetVideoSettings');
        if (videoSettings && videoSettings.baseWidth && videoSettings.baseHeight) {
            canvasWidth = videoSettings.baseWidth; canvasHeight = videoSettings.baseHeight;
        }
    } catch (error) { logMessage(`Canvas size error: ${error.message}, using default.`); }

    const inputSettings = { url: vdoNinjaStreamUrl, width: canvasWidth, height: canvasHeight, fps: 30, reroute_audio: true, restart_when_active: false, shutdown: false };

    logMessage(`Adding source '${sourceName}' to scene '${targetSceneName}'. Cloning to main: ${shouldCloneThisStreamToMain}. Switching: ${shouldSwitchToThisScene}`);

    try {
        let sourceExistsGlobally = false;
        let initialSceneItemId = null;
        let clonedSceneItemId = null;

        try {
            const sources = await sendRequest('GetInputList');
            sourceExistsGlobally = sources.inputs.some(input => input.inputName === sourceName);
        } catch (error) { logMessage(`Error checking global source list: ${error.message}`); }

        if (!sourceExistsGlobally) {
            // Create the input and add it to the target scene
            const createInputResponse = await sendRequest('CreateInput', { 
                sceneName: targetSceneName,
                inputName: sourceName, 
                inputKind: 'browser_source', 
                inputSettings, 
                sceneItemEnabled: true 
            });
            logMessage(`Source '${sourceName}' created and added to scene '${targetSceneName}'.`);
            
            // Get the scene item ID that was just created
            try {
                const itemInfo = await sendRequest('GetSceneItemId', { 
                    sceneName: targetSceneName, 
                    sourceName: sourceName 
                });
                initialSceneItemId = itemInfo.sceneItemId;
                logMessage(`Source item ID: ${initialSceneItemId}`);
            } catch (e) {
                logMessage(`Couldn't get scene item ID for new source: ${e.message}`);
            }
            
            // Clone to main scene if needed
            if (shouldCloneThisStreamToMain && mainDefaultScene && mainDefaultScene !== targetSceneName) {
                try {
                    const createCloneResponse = await sendRequest('CreateSceneItem', {
                        sceneName: mainDefaultScene, 
                        sourceName: sourceName
                    });
                    clonedSceneItemId = createCloneResponse.sceneItemId;
                    logMessage(`Source '${sourceName}' cloned to main scene '${mainDefaultScene}'. Item ID: ${clonedSceneItemId}`);
                } catch (error) {
                    logMessage(`Error cloning source to main scene '${mainDefaultScene}': ${error.message}`);
                }
            }
        } else {
            // Update existing input settings
            await sendRequest('SetInputSettings', { inputName: sourceName, inputSettings });
            logMessage(`Settings updated for existing source '${sourceName}'.`);
            
            // Check if already in target scene
            try {
                const itemInfo = await sendRequest('GetSceneItemId', { sceneName: targetSceneName, sourceName: sourceName });
                initialSceneItemId = itemInfo.sceneItemId;
                logMessage(`Existing source '${sourceName}' already in scene '${targetSceneName}'. Item ID: ${initialSceneItemId}`);
            } catch (e) {
                if (e.message.toLowerCase().includes("not found")) {
                    const createItemResponse = await sendRequest('CreateSceneItem', { sceneName: targetSceneName, sourceName: sourceName });
                    initialSceneItemId = createItemResponse.sceneItemId;
                    logMessage(`Existing source '${sourceName}' added to scene '${targetSceneName}'. Item ID: ${initialSceneItemId}`);
                } else { throw e; }
            }
            
            // Check if already in main scene or add if cloning
            if (shouldCloneThisStreamToMain && mainDefaultScene && mainDefaultScene !== targetSceneName) {
                try {
                    const existingItemInfo = await sendRequest('GetSceneItemId', { sceneName: mainDefaultScene, sourceName: sourceName });
                    clonedSceneItemId = existingItemInfo.sceneItemId;
                    logMessage(`Source '${sourceName}' already in main scene '${mainDefaultScene}'. Item ID: ${clonedSceneItemId}`);
                } catch (e) {
                    if (e.message.toLowerCase().includes("not found")) {
                        try {
                            const createCloneResponse = await sendRequest('CreateSceneItem', { sceneName: mainDefaultScene, sourceName: sourceName });
                            clonedSceneItemId = createCloneResponse.sceneItemId;
                            logMessage(`Source '${sourceName}' cloned to main scene '${mainDefaultScene}'. Item ID: ${clonedSceneItemId}`);
                        } catch (createError) {
                            logMessage(`Error creating clone in main scene '${mainDefaultScene}': ${createError.message}`);
                        }
                    } else {
                        logMessage(`Error checking main scene '${mainDefaultScene}': ${e.message}`);
                    }
                }
            }
        }

        if (activeStreams[streamId]) activeStreams[streamId].sourceCreated = true;
        updateStreamList();

        // Apply transforms
        if (initialSceneItemId) {
            await applyTransformAndGrid(targetSceneName, sourceName, canvasWidth, canvasHeight, initialSceneItemId);
        }

        if (clonedSceneItemId) {
            await applyTransformAndGrid(mainDefaultScene, sourceName, canvasWidth, canvasHeight, clonedSceneItemId);
        }
        
        if (shouldSwitchToThisScene) {
            await sendRequest('SetCurrentProgramScene', { sceneName: targetSceneName });
            logMessage(`Switched to scene '${targetSceneName}'.`);
        }

    } catch (error) { 
        logMessage(`Error adding stream '${sourceName}' to OBS: ${error.message}`); 
        if (activeStreams[streamId]) activeStreams[streamId].sourceCreated = false;
        updateStreamList();
    }
}

async function applyTransformAndGrid(sceneName, sourceName, canvasWidth, canvasHeight, sceneItemId = null) {
    if (!sceneName) {
        logMessage(`Cannot apply transform: Scene name not provided for source '${sourceName}'.`);
        return;
    }
    let itemIdToTransform = sceneItemId;

    if (!itemIdToTransform) { // If ID not provided, try to fetch it
        try {
            logMessage(`Workspaceing scene item ID for '${sourceName}' in '${sceneName}' for transform (ID not passed).`);
            const itemInfo = await sendRequest('GetSceneItemId', { sceneName, sourceName });
            if (itemInfo && itemInfo.sceneItemId) {
                itemIdToTransform = itemInfo.sceneItemId;
            } else {
                // This case should ideally not be hit if creation was successful
                logMessage(`Could not find scene item ID for '${sourceName}' in '${sceneName}' (fetch attempt).`);
                return; 
            }
        } catch (error) {
            // This is where the user's logged error "OBS Request Error (GetSceneItemId): No scene items were found..." likely originated
            logMessage(`Error fetching scene item ID for transform of '${sourceName}' in '${sceneName}': ${error.message}`);
            return; 
        }
    }

    // Proceed with itemIdToTransform
    if (!itemIdToTransform) {
        logMessage(`Cannot apply transform for '${sourceName}' in '${sceneName}': Valid Scene Item ID not available.`);
        return;
    }

    if (sourceSizingSelect.value === 'autoGrid') {
        // rearrangeAllStreamsInScene will handle transforms for all relevant items in the grid.
        // It's generally better to call this once after all items are added/removed for a scene.
        await rearrangeAllStreamsInScene(sceneName); 
    } else {
        try {
            const transform = calculateTransform(sourceSizingSelect.value, canvasWidth, canvasHeight, canvasWidth, canvasHeight);
            await sendRequest('SetSceneItemTransform', { sceneName, sceneItemId: itemIdToTransform, sceneItemTransform: transform });
            logMessage(`Applied non-grid transform to '${sourceName}' (Item ID: ${itemIdToTransform}) in '${sceneName}'.`);
        } catch (error) {
            logMessage(`Error applying non-grid transform to '${sourceName}' (Item ID: ${itemIdToTransform}) in '${sceneName}': ${error.message}`);
        }
    }
}


async function rearrangeAllStreamsInScene(sceneName) {
    if (!obsConnected || !obs || !sceneName || sourceSizingSelect.value !== 'autoGrid') return;
    logMessage(`Rearranging streams in scene '${sceneName}' using autoGrid.`);
    try {
        let canvasWidth = 1920, canvasHeight = 1080;
        try {
            const videoSettings = await sendRequest('GetVideoSettings');
            if (videoSettings && videoSettings.baseWidth && videoSettings.baseHeight) {
                canvasWidth = videoSettings.baseWidth; canvasHeight = videoSettings.baseHeight;
            }
        } catch (error) { logMessage(`Canvas size error for rearrange: ${error.message}`); }

        const sceneItemsResponse = await sendRequest('GetSceneItemList', { sceneName });
        const vdoNinjaSourcesInScene = sceneItemsResponse.sceneItems.filter(item => 
            item.sourceName.startsWith('VDO.Ninja_') && 
            Object.values(activeStreams).some(as => `VDO.Ninja_${as.streamId}` === item.sourceName && as.connected) // Only active, connected VDO.Ninja streams
        );

        if (vdoNinjaSourcesInScene.length === 0) { 
            logMessage(`No active VDO.Ninja sources found in scene '${sceneName}' to rearrange.`);
            return;
        }
        
        const positions = calculateGridPositions(vdoNinjaSourcesInScene.length, canvasWidth, canvasHeight);
        for (let i = 0; i < vdoNinjaSourcesInScene.length; i++) {
            const item = vdoNinjaSourcesInScene[i];
            const transform = calculateTransform('autoGrid', canvasWidth, canvasHeight, canvasWidth, canvasHeight, positions[i]);
            await sendRequest('SetSceneItemTransform', { sceneName, sceneItemId: item.sceneItemId, sceneItemTransform: transform });
        }
        logMessage(`Rearranged ${vdoNinjaSourcesInScene.length} sources in scene '${sceneName}'.`);
    } catch (error) { logMessage(`Error rearranging streams in scene '${sceneName}': ${error.message}`); }
}


function calculateTransform(sizingMode, sourceWidth, sourceHeight, canvasWidth, canvasHeight, gridPosition = null) {
    let transform = { alignment: 5, boundsType: "OBS_BOUNDS_NONE", boundsAlignment: 0, boundsWidth: sourceWidth, boundsHeight: sourceHeight, positionX: 0, positionY: 0, scaleX: 1.0, scaleY: 1.0, rotation: 0.0, cropTop: 0, cropBottom: 0, cropLeft: 0, cropRight: 0, sourceWidth, sourceHeight, width: sourceWidth, height: sourceHeight };
    switch (sizingMode) {
        case 'stretchToFill': transform.boundsType = "OBS_BOUNDS_STRETCH"; transform.boundsWidth = canvasWidth; transform.boundsHeight = canvasHeight; transform.width = canvasWidth; transform.height = canvasHeight; break;
        case 'bestFit':       transform.boundsType = "OBS_BOUNDS_SCALE_INNER"; transform.boundsWidth = canvasWidth; transform.boundsHeight = canvasHeight; transform.width = canvasWidth; transform.height = canvasHeight; break;
        case 'autoGrid':
            if (gridPosition) {
                transform.boundsType = "OBS_BOUNDS_SCALE_INNER"; // Fit within the grid cell
                transform.positionX = gridPosition.x; transform.positionY = gridPosition.y;
                transform.boundsWidth = gridPosition.width; transform.boundsHeight = gridPosition.height;
                transform.width = gridPosition.width; transform.height = gridPosition.height;
            } else { // Fallback if no grid position (shouldn't happen if called correctly)
                transform.boundsType = "OBS_BOUNDS_SCALE_INNER"; transform.boundsWidth = canvasWidth; transform.boundsHeight = canvasHeight; transform.width = canvasWidth; transform.height = canvasHeight;
            }
            break;
        case 'defaultSize':
        default: transform.positionX = (canvasWidth - sourceWidth) / 2; transform.positionY = (canvasHeight - sourceHeight) / 2; break;
    }
    return transform;
}

async function removeStreamFromObs(streamId) {
    if (!obsConnected || !obs) return;
    const sourceName = `VDO.Ninja_${streamId}`;
    logMessage(`Attempting to remove source '${sourceName}' from OBS.`);

    const streamInfo = activeStreams[streamId];
    const targetInfo = streamInfo ? getTargetSceneForStream(streamId, streamInfo.label) : getTargetSceneForStream(streamId, '');
    
    const scenesToCheck = new Set();
    if (targetInfo.scene) scenesToCheck.add(targetInfo.scene); // Mapped or default scene
    scenesToCheck.add(getTargetScene()); // Always check the main selected scene

    // If it was potentially cloned from a mapping
    if (targetInfo.mapping && targetInfo.mapping.cloneToMain && targetInfo.scene !== getTargetScene()) {
        scenesToCheck.add(getTargetScene()); // Add main scene if it was cloned there
    }
    
    let removedFromAnyScene = false;
    for (const sceneName of scenesToCheck) {
        if (await tryRemoveFromScene(sourceName, sceneName)) {
            removedFromAnyScene = true;
        }
    }

    // Optional: If not found in likely scenes, consider removing the input globally if no longer in any scene item.
    // This is more complex as it requires checking all scenes. For now, just removing from target/main.
    // If you want to remove the input itself if it's not used anywhere:
    // 1. Check if the source item exists in *any* scene.
    // 2. If not, then call sendRequest('RemoveInput', { inputName: sourceName });
    // This is usually not necessary as OBS handles unused sources gracefully.

    if (!removedFromAnyScene) {
        logMessage(`Source '${sourceName}' not found in relevant scenes for removal or already removed.`);
    }
    // Rearrange streams in affected scenes
    if (sourceSizingSelect.value === 'autoGrid') {
        for (const sceneName of scenesToCheck) {
            setTimeout(() => rearrangeAllStreamsInScene(sceneName), 200); // Delay for removal to process
        }
    }
}

async function tryRemoveFromScene(sourceName, sceneName) {
    if (!sceneName) return false;
    try {
        const itemInfo = await sendRequest('GetSceneItemId', { sceneName, sourceName });
        if (itemInfo && itemInfo.sceneItemId) {
            await sendRequest('RemoveSceneItem', { sceneName, sceneItemId: itemInfo.sceneItemId });
            logMessage(`Removed source '${sourceName}' from scene '${sceneName}'.`);
            return true;
        }
    } catch (error) {
        if (!error.message.toLowerCase().includes("not found")) { // Log only unexpected errors
            logMessage(`Error trying to remove '${sourceName}' from '${sceneName}': ${error.message}`);
        }
    }
    return false;
}

function loadJsShaLibrary() {
    return new Promise((resolve, reject) => {
        if (typeof jsSHA !== 'undefined') { resolve(); return; }
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jsSHA/3.3.0/sha256.js'; // Ensure this is a reliable CDN
        script.onload = resolve;
        script.onerror = () => reject(new Error('Failed to load jsSHA library'));
        document.head.appendChild(script);
    });
}

// Event handlers
obsSceneSelect.addEventListener('change', () => { obsSceneNameInput.value = obsSceneSelect.value; saveSettings(); });
[obsWsUrlInput, obsWsPasswordInput, vdoNinjaRoomInput, vdoNinjaPasswordInput, vdoNinjaStreamIdsInput, autoAddSourcesCheckbox, autoRemoveSourcesCheckbox].forEach(el => el.addEventListener('change', saveSettings));
sourceSizingSelect.addEventListener('change', () => {
    saveSettings();
    if (sourceSizingSelect.value === 'autoGrid') {
        rearrangeAllStreamsInScene(getTargetScene()); // Rearrange current target scene
    }
});
loadScenesBtn.addEventListener('click', fetchObsScenes);

document.addEventListener('DOMContentLoaded', () => {
    loadSettings(); 
    logMessage("VDO.Ninja OBS Control Dock Initialized.");
    const secureFields = document.querySelectorAll('#vdoNinjaRoom, #vdoNinjaStreamIds');
    secureFields.forEach(field => {
        field.classList.add('blur-field');
        field.addEventListener('focus', () => field.classList.remove('blur-field'));
        field.addEventListener('blur', () => field.classList.add('blur-field'));
    });
    if (!document.getElementById('autoRemoveSources')) { // Defensive check
        const autoSourceOptions = document.getElementById('autoSourceOptions');
        if (autoSourceOptions) { /* ... ensure it's created if somehow missing ... */ }
    }
    startVdoNinjaConnectionMonitor();
});
</script>
</body>
</html>